# -*- coding: utf-8 -*-
"""Misc utilities"""

import datetime
import urlparse
import urllib
import cgi
import os
import types
import smtplib
import random
import re
import hashlib
import time
import calendar

import turbogears
import genshi

import formencode
from formencode import validators
import mnemoply

def my_import(name):
    mod = __import__(name)
    components = name.split('.')
    for comp in components[1:]:
        mod = getattr(mod, comp)
    return mod

def escape(string):
    return cgi.escape(unicode(string), True)

# note: don't like multiple values in url(i.e. a=c&a=d)
def replace_query_var(url, **kw):
    (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
    # i.e. ['a': 'b', 'c': 'd']
    args = {}
    if len(query) > 0:
        for k, v in cgi.parse_qs(query, keep_blank_values=True).items():
            args[k] = v[0]

    args.update(kw)

    query = '&'.join([arg[0] + '=' + urllib.quote_plus(str(arg[1])) for arg in args.items()])
    return urlparse.urlunsplit((scheme, netloc, path, query, fragment))



def urlparse_qs(url, keep_blank_values=0, strict_parsing=0):
    """Parse a URL query string and return the components as a dictionary.

	Based on the cgi.parse_qs method.This is a utility function provided
	with urlparse so that users need not use cgi module for
	parsing the url query string.

        Arguments:

        url: URL with query string to be parsed

        keep_blank_values: flag indicating whether blank values in
            URL encoded queries should be treated as blank strings.
            A true value indicates that blanks should be retained as
            blank strings.  The default false value indicates that
            blank values are to be ignored and treated as if they were
            not included.

        strict_parsing: flag indicating what to do with parsing errors.
            If false (the default), errors are silently ignored.
            If true, errors raise a ValueError exception.
    """

    myUnquote = lambda s: urllib.unquote(str(s)).replace('+', ' ')

    scheme, netloc, url, params, querystring, fragment = urlparse.urlparse(url)

    pairs = [s2 for s1 in url.split('&') for s2 in s1.split(';')]
    query = []
    for name_value in pairs:
        if not name_value and not strict_parsing:
            continue
        nv = name_value.split('=', 1)
        if len(nv) != 2:
            if strict_parsing:
                raise ValueError, "bad query field: %r" % (name_value,)
            # Handle case of a control-name with no equal sign
            if keep_blank_values:
                nv.append('')
            else:
                continue
        if len(nv[1]) or keep_blank_values:
            name = myUnquote(nv[0])
            value = myUnquote(nv[1])
            query.append((name, value))

    result = {}
    for name, value in query:
        if name in result:
            result[name].append(value)
        else:
            result[name] = [value]
    for x in result:
        if len(result[x]) == 1:
            result[x] = result[x][0]
    return result

class Wrapper(object):
    """
    Wraps up dictionaries, internal class. Lets you use getattr
    just like getitem.
    """
    def __init__(self, _wrapped=None, **kw):
        if _wrapped:
            self._d = _wrapped
        else:
            self._d = {}
        self._d.update(kw)
    def __delitem__(self, key):
        del self._d[key]
    def __getattribute__(self, key):
        try:
            return object.__getattribute__(self, key)
        except AttributeError:
            try:
                return self[key]
            except KeyError, exc:
                raise AttributeError, exc

    def __getitem__(self, key):
        return self._d[key]
    def __setitem__(self, key, value):
        self._d[key] = value
    def __contains__(self, key):
        return self._d.__contains__(key)
    def __setattr__(self, key, value):
        if key[0] == "_":
            object.__setattr__(self, key, value)
        else:
            self._d[key] = value
    def pop(self, key):
        return self._d.pop(key)
    def get(self, *args, **kwargs):
        return self._d.get(*args, **kwargs)
    def clear(self):
        self._d.clear()
    def keys(self):
        return self._d.keys()
    def items(self):
        return self._d.items()
    def values(self):
        return self._d.values()
    def has_key(self, k):
        return self._d.has_key(k)
    def __repr__(self):
        return self._d.__repr__()
    def __str__(self):
        return self._d.__str__()
    def get_dict(self):
        return self._d

# ASPN enum
def make_enum(*names):
    assert names, "Empty enums are not supported"

    class EnumClass(object):
        __slots__ = names

        def __iter__(self):
            return iter(constants)

        def __len__(self):
            return len(constants)

        def __getitem__(self, i):
            return constants[i]

        def __repr__(self):
            return 'Enum' + str(names)

        def __str__(self):
            return 'enum ' + str(constants)

    class EnumValue(object):
        __slots__ = ('__value')

        def __init__(self, value):
            self.__value = value

        value = property(lambda self: self.__value)
        enum_type = property(lambda self: enum_type)

        def __hash__(self):
            return hash(self.__value)
        def __cmp__(self, other):
            if other is None:
                return 1

            if isinstance(other, str) or isinstance(other, unicode):
                #if other == '':
                #    return 1
                return cmp(self.__value, other)

            assert self.enum_type is other.enum_type, \
                "Only values from the same enum are comparable"
            return cmp(self.__value, other.__value)
        def __invert__(self):
            return constants[maximum - self.__value]
        def __nonzero__(self):
            return bool(self.__value)
        def __repr__(self):
            return str(names[self.__value])

    maximum = len(names) - 1
    constants = [None] * len(names)
    for i, each in enumerate(names):
        val = EnumValue(i)
        setattr(EnumClass, each, val)
        constants[i] = val
    constants = tuple(constants)
    enum_type = EnumClass()
    return enum_type

class Delegate:
    def __init__(self, callback=None):
        self._callbacks = {}
        if callback:
            self._callbacks[callback] = None

    def __add__(self, other):
        new = Delegate()
        new._callbacks.update(self._callbacks)

        if isinstance(other, Delegate):
            new._callbacks.update(other._callbacks)
        else:
            new._callbacks[other] = None

        return new

    def __call__(self, *args, **kw):
        for callback in self._callbacks.keys():
            callback(*args, **kw)

    def __sub__(self, other):
        new = Delegate()
        new._callbacks.update(self._callbacks)
        if isinstance(other, Delegate):
            for key in other._callbacks.keys():
                try:
                    del new._callbacks[key]
                except AttributeError:
                    pass
        else:
            if new._callbacks.has_key(other):
                try:
                    del new._callbacks[other]
                except AttributeError:
                    pass
        return new

class MonthYear:
    def __init__(self, month=datetime.date.today().month, year=datetime.date.today().year):
        self.month = month
        self.year = year

    def __str__(self):
        return "%02d.%04d" % (self.month, self.year)

    def to_int(self):
        return self.month - 1 + (self.year * 12)

    @classmethod
    def from_int(cls, i):
        i = int(i)
        return cls(month=(i % 12) + 1, year=i / 12)

    @classmethod
    def iterate(cls, from_my, to_my):
        for i in range(from_my.to_int(), to_my.to_int() + 1):
            yield cls.from_int(i)

    @classmethod
    def get_academic_year_begin(cls):
        now = datetime.date.today()
        if now.month <= 8:
            return MonthYear(month=9, year=now.year - 1)
        else:
            return MonthYear(month=9, year=now.year)

    @classmethod
    def get_academic_year_end(cls):
        now = datetime.date.today()
        if now.month <= 8:
            return MonthYear(month=5, year=now.year)
        else:
            return MonthYear(month=5, year=now.year + 1)

    def get_begin_month(self):
        return datetime.date(self.year, self.month, 1)

    def get_end_month(self):
        next = MonthYear.from_int(self.to_int() + 1)
        return next.get_begin_month() - datetime.timedelta(days=1)

    def get_next_month_year(self):
        if self.month <= 11:
            return dict(year=self.year, month=self.month+1)
        if self.month == 12:
            return dict(year=self.year+1, month=1)

    def next_month(self):
        if self.month <= 11:
            return MonthYear(year=self.year, month=self.month+1)
        if self.month == 12:
            return MonthYear(year=self.year+1, month=1)

    def get_last_day(self):
        return last_month_day(self.month, self.year)

    def __eq__(self, another_month_year):
        y = (self.year == another_month_year.year)
        m = (self.month == another_month_year.month)
        if y and m:
            return True
        return False

    def __ne__(self, another_month_year):
        return not(self == another_month_year)

def write_file(name, content, erase_empty=False):
    if erase_empty and (not content or len(content) == 0):
        try:
            os.unlink(name)
        except OSError:
            pass
        return

    fobj = open(name, "w")
    fobj.write(content)
    fobj.close()

def read_file(name, default=None):
    try:
        fobj = open(name, 'r')
    except IOError:
        if not default is None:
            return default
        raise

    content = fobj.read()
    fobj.close()

    return content

def format_currency(decimal):
    if decimal is None:
        return ''
    else:
        ### BUGBUG Take format from cfg file?
        return "%.2f" % float(decimal)

def format_date(d=None):
    if d is None:
        d = datetime.date.today()
    return d.strftime("%d.%m.%Y")

def send_mail(to_addr, subject, body):
    from_addr = turbogears.config.get("from_addr")
    msg = ("From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain\r\n\r\n%s"
           % (from_addr, to_addr, subject, body))

    server = smtplib.SMTP(turbogears.config.get("smtp_host"))
#    server.set_debuglevel(1)
    server.sendmail( from_addr, [to_addr], msg)
    server.quit()

def report_error(subject, body):
    assigned_id = random.randrange(100000)

    # Send mail to the developer
    send_mail(turbogears.config.get("developer_addr"),
        "ERROR REPORT %d: " % assigned_id + subject, body)

    return assigned_id

def shift_date(date, days):
    return datetime.date.fromordinal(
        date.toordinal() + days)

# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/438823
class odict(dict):
    def __init__(self, d=None):
        if d is None:
            d = {}
        self._keys = d.keys()
        dict.__init__(self, d)

    def __delitem__(self, key):
        dict.__delitem__(self, key)
        self._keys.remove(key)

    def __setitem__(self, key, item):
        dict.__setitem__(self, key, item)
        # a peculiar sharp edge from copy.deepcopy
        # we'll have our set item called without __init__
        if not hasattr(self, '_keys'):
            self._keys = [key, ]
        if key not in self._keys:
            self._keys.append(key)

    def clear(self):
        dict.clear(self)
        self._keys = []

    def items(self):
        for i in self._keys:
            yield i, self[i]

    def keys(self):
        return self._keys

    def popitem(self):
        if not len(self._keys):
            raise KeyError('dictionary is empty')
        else:
            key = self._keys[-1]
            val = self[key]
            del self[key]
            return key, val

    def setdefault(self, key, failobj = None):
        dict.setdefault(self, key, failobj)
        if key not in self._keys:
            self._keys.append(key)

    def update(self, d):
        for key in d.keys():
            if not self.has_key(key):
                self._keys.append(key)
        dict.update(self, d)

    def values(self):
        for i in self._keys:
            yield self[i]

    def move(self, key, index):

        """ Move the specified to key to *before* the specified index. """

        try:
            cur = self._keys.index(key)
        except ValueError:
            raise KeyError(key)
        self._keys.insert(index, key)
        # this may have shifted the position of cur, if it is after index
        if cur >= index:
            cur += 1
        del self._keys[cur]

    def index(self, key):
        if not self.has_key(key):
            raise KeyError(key)
        return self._keys.index(key)

class LocalMenuItem(Wrapper):
    def isShown(self):
        return getattr(self, 'condition', lambda: True)()

class LocalMenu(object):
    def __init__(self):
        # I know, this method would be called more than once. Dirty workaround.
        if not hasattr(self, 'local_menu'):
            self.local_menu = []
        # Container's object
        if not hasattr(self, 'object_local_menu'):
            self.object_local_menu = []

class ExceptionsBunch(Exception):
    def __init__(self, pairs):
        Exception.__init__(self)

        self.pairs = pairs
        self.title = ""

class MigrationException(Exception):
    def __init__(self, what):
        Exception.__init__(self)
        self.what = what

    def __str__(self):
        return self.what

    def __unicode__(self):
        return self.what

class IdsValidator(validators.FancyValidator):
    not_empty = True

    messages = {
        'idlist': "Please enter id list (values separated by comma)",
    }

    def _to_python(self, value, state):
        try:
            assert isinstance(value, types.StringTypes)
            return [int(x) for x in value.split(',')]
        except (AssertionError, ValueError, TypeError):
            raise formencode.Invalid(self.message('idlist', state),
                          value, state)

    def _from_python(self, value, state):
        assert isinstance(value, types.ListType)
        return ','.join([str(int(x)) for x in value])

def gen_random_id():
    return str(random.randrange(100000))

def objs2ids(objs):
    return ",".join([str(x.id) for x in objs])

def ids2objs(ids, cls):
    if not ids:
        return []
    return [cls.get(int(x)) for x in ids.split(',')]

# ugly but works
def parse_flash(flash):
    match = re.search(r'link:(\S+):([A-Za-z/0-9]*)', flash)

    if match:
        flash = re.sub(r'link:(\S+):([A-Za-z/0-9]*)', '', flash)
        return genshi.XML('<span>%s <a href="%s">%s</a></span>' % (cgi.escape(flash),
                                                                   cgi.escape(match.group(2), True), match.group(1)))
    return flash

def getOne(sresults, default=None):
    try:
        return sresults.__iter__().next()
    except StopIteration:
        return default

def md5(data):
    m = hashlib.md5()
    m.update(data)
    return m.hexdigest()

def sha224(data):
    h = hashlib.sha224()
    h.update(data)
    return h.hexdigest()

def str2date(string):
    return time.strptime(string, "%Y-%m-%d %H:%M:%S")

def str2date2(string):
    return datetime.date(*time.strptime(string, "%d.%m.%Y")[0:3])

def date2str(date):
    return date.strftime("%Y-%m-%d")

def date2str_points(date):
    if date.year <= 1900:  # TODO: remove hack
        return date
    return date.strftime("%d.%m.%Y")

def last_month_day(_month, _year):
    end_of_months = [0] + [calendar.monthrange(year, month)[1] for year in [_year] for month in range(1, 13)]
    return "%02d.%02d.%04d" % (end_of_months[_month], _month, _year)

def last_month_day_only(_month, _year):
    end_of_months = [0] + [calendar.monthrange(year, month)[1] for year in [_year] for month in range(1, 13)]
    return end_of_months[_month]

def getHalfMonthBeforeToday():
    date = datetime.date.today()
    if date.day > 15:
        if date.month == 1:
            beginning_date = datetime.date(year=date.year-1, month=12, day=last_month_day_only(12, date.year-1))
        else:
            beginning_date = datetime.date(year=date.year, month=date.month-1, day=last_month_day_only(date.month - 1, date.year))
    else:
        if date.month == 1:
            beginning_date = datetime.date(year=date.year-1, month=12, day=15)
        else:
            beginning_date = datetime.date(year=date.year, month=date.month-1, day=15)
    ending_date = next_day(date)
    return beginning_date, ending_date

def get_half_month_before_current_day():
    beginning_date, ending_date = getHalfMonthBeforeToday()
    return "between " + beginning_date.strftime("%Y-%m-%d ") + ending_date.strftime("%Y-%m-%d")

def parsing_between(string):
    tokens = string.split()
    try:
        if tokens[0] == 'between' and len(list(tokens)) == 3:
            return tokens[1], tokens[2]
    except IndexError:
        return None

def next_day(date):
    if date.day != last_month_day_only(date.month, date.year):
        return datetime.date(year=date.year, month=date.month, day=date.day+1)
    elif date.month != 12:
        return datetime.date(year=date.year, month=date.month+1, day=1)
    else:
        return datetime.date(year=date.year+1, month=1, day=1)

def isint(source):
    try:
        int(source.strip().split()[0])
    except (ValueError, IndexError):
        return False
    return True

def copyInstance(instance):
    data = {}
    for k, w in instance.sqlmeta.columns.iteritems():
        data[k] = getattr(instance, k)

    return data

def getBarcodeImage(barcode, path="barcodes"):
    try:
        from subprocess import call
        # check input
        if not barcode.isdigit():
            raise Exception("Wrong input")

        eps = path + os.sep + "%s.eps" % barcode
        png = path + os.sep + "%s.png" % barcode

        call(["barcode", "-b"+barcode, "-o"+eps, "-eEAN"])
        call(["convert", eps, png])
        call(["convert", png, "-crop", "72x52+8+740", png])

        if os.path.exists(eps):
            os.remove(eps)

        return png

    except Exception:
        raise Exception("Barcode generator doesn't works")

def read_binary_file(name):
    binary_file = None
    try:
        binary_file = open(name, 'rb')
        content = binary_file.read()
        return content
    finally:
        if binary_file:
            binary_file.close()

def checkBarcodeGenerator():
    from os.path import exists
    try:
        getBarcodeImage("1234567", ".")
        if not exists("1234567.png"):
            raise Exception("Barcode file not found")
    finally:
        if exists("1234567.png"):
            os.remove("1234567.png")

def cleanBarcodeDir():
    path = "./barcodes"
    ext = ".png"
    for f in os.listdir(path):
        if f.endswith(ext):
            os.remove(os.path.join(path, f))

def getGroups(user):
    """
        Returns groups ID, where user's person is teacher.
    """
    if user.person is None:
        return []
    else:
        return [g.id for g in mnemoply.model.Group.selectBy(teacher=user.person)]

def ifReferFromGroup(url):
    """
        Takes URL and returns True if URL like 'host/Group/279/' or 'host/Group/279'
    """
    tokens = url.split('/')

    non_empty_tokens = [t for t in tokens if t != ""]
    try:
        if non_empty_tokens[::-1][1] == "Group": # list[::-1] it's reversed list
            return True
        else:
            return False
    except:
        return False

def getGroupFromRefer(url):
    """
        Takes URL and returns item number if URL like 'host/Group/279/' or 'host/Group/279'
    """
    tokens = url.split('/')

    non_empty_tokens = [t for t in tokens if t != ""]
    try:
        return int(non_empty_tokens[::-1][0]) # list[::-1] it's reversed list
    except:
        return 0

def timedelta2int(d):
    return d.days *60*60*24 + d.seconds

def getDocumentType(student):
    from mnemoply import model
    from sqlobject.sqlbuilder import AND

    marks = list(model.Mark.selectBy(student=student))
    final_work_marks = [m for m in model.FinalWorkMark.selectBy(student=student) if not m.markType.bad_mark]
    hours = sum([m.hours for m in final_work_marks]) + sum([m.hours for m in marks])
    works_amount = len(final_work_marks)

    document = None
    dt = model.DocumentType
    documents = dt.select(
        AND(dt.q.minHours <= hours, dt.q.minWorks <= works_amount),
        orderBy=[-dt.q.minHours, -dt.q.minWorks])
    if len(list(documents)):
        document = documents[0]

    final_work_marks.sort(cmp=lambda x, y: timedelta2int(y.date - x.date)) # TODO sort this list in pure SQL
    last_final_topic = None
    if len(final_work_marks):
        last_final_topic = final_work_marks[0].topic

    return document, hours, works_amount, last_final_topic

def planSortFunction(l):
    return l.sort(key=lambda y: (y.trend.title, -y.year))

def applyDecimalDiscont(base, discont):
    if discont is not None:
        if discont.isPercent:
            return base * (100 - discont.percent) / 100
        elif discont.amount is not None:
            return base - discont.amount

    return base

def applyDiscont(base, discont):
    return int(round(applyDecimalDiscont(base, discont)))

def time2str(value):
    return "%02d:%02d" % (value.hour, value.minute)

def getTime(hours, minutes):
    return datetime.time(hours, minutes)

def isPointInInterval(start, end, point):
    """
        If poind inside interval returns True. Else returns False.
    """

    if start <= point <= end:
        return True
    return False

def isIntersect(start1, end1, start2, end2):
    """
        Takes 2 intervals, givens by theirs begins and ends.
        Returns True, if these intervals intersects.
        Else returns False.
    """

    def checkFirstInterval(s1, e1, s2, e2):
        return isPointInInterval(s1, e1, s2) or isPointInInterval(s1, e1, e2)
    return checkFirstInterval(start1, end1, start2, end2) or checkFirstInterval(start2, end2, start1, end1)

def getMondayBeforeThat(d=datetime.date.today()):
    """
        Returns first day of week before `d`.
        Returns datetime.date, not datetime.datetime.
        Parameter d can be datetime.datetime or datetime.date
    """
    result = d - datetime.timedelta(days=d.weekday())
    if isinstance(result, datetime.datetime): # allows to avoid an error when param d is date (default value)
        result = result.date()
    return result

def update_with(src, ext):
    """Update source list with extension."""
    res = src
    for e in ext:
        if e not in src:
            src += [e]
    return res

def cleanup_kw(kw):
    """Replace fuckin None to u''."""
    for k in kw.keys():
        if kw[k] is None:
            kw[k] = u''
    return kw

def replace_empty_to_none(kw):
    """Replace u'' to None."""
    for k in kw.keys():
        if kw[k] is u'':
            kw[k] = None
    return kw

def get_id_or_empty(obj):
    """Get object str(id) if object exists, else returns empty string."""
    if obj:
        return str(obj.id)
    return ''

def drop_key_if_exists(kw, key):
    if kw and key in kw:
        del kw[key]
    return kw

class PhoneFormat(object):
    @staticmethod
    def bad_str(rus=False):
        if (rus):
            return _('Incorrectly dialed')
        return 'Incorrectly dialed'
    @staticmethod
    def to_python(value):
    
        def is_phone_from_dict(value):
            if not isinstance(value, dict):
                return False
            if not (value.has_key('code') and value.has_key('num')):
                return False
            new_value = check_empty(value)
            if len(new_value['code']) + len(new_value['num'])!=10:
                return False
            return True
                
        def check_empty(dict_value):
            num = str(dict_value['num']).strip()
            code = str(dict_value['code']).strip()
            if not (len(code) + len(num)):
                code = '000'
                num = '0000000'
            if not len(code):
                code = '0'*(10-len(num))
            if not len(num):
                num = '0'*(10-len(code))
            return dict(code=code,num=num)
        if value is None:
            return None
        if not (is_phone_from_dict(value)):
            return PhoneFormat.bad_str()
        new_value = check_empty(value)
        num = new_value['num']
        code = new_value['code']
        num_full = "%s-%s-%s"%(num[:-4],num[-4:-2],num[-2:])
        return "+7-(%s)-%s"%(code,num_full) 
    
    @staticmethod
    def from_python(value):
        if value is None:
            return None
        beg_num = value.find(')')
        return dict (code=value[4:beg_num],num=value[beg_num+2:].replace('-',''))
